﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;
using System.Xml;
using System.Windows.Forms;
using System.Reflection;
using DotSpatial.Controls;
using DotSpatial.Controls.Header;
using DotSpatial.Data;
using DotSpatial.Topology;
using System.Data;
using DotSpatial.Symbology;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;


namespace TerrainViewerPluginDotSpatial
{
    /// <summary>
    /// This is the main class for the 3d-viewer
    /// </summary>
    public class Class3d
    {      
        private string _snapshotImageName1 = "";
        private string _snapshotImageName2 = "";
        private string _snapshotRasterName = "";
        private string _bilFileName = "";
        private string _xmlFileName = "";
        private string _terrainViewerExeFile = "";
        private string _boundaryLayerName = "3d-view-boundary";
        private int SNAPSHOT_GRID_SIZE = 512; //the snapshot grid size must be 512. Other size don't work.
        private Raster _originalGrid = null;
        private Map _mainMap = null;

        /// <summary>
        /// The large snapshot bmp file that is used as overlay
        /// of the 3d grid
        /// </summary>
        public string SnapshotImageName1 
        {
            get { return _snapshotImageName1; }
            set { _snapshotImageName1 = value; }
        }

        /// <summary>
        /// The small snapshot bmp file for the overview map
        /// </summary>
        public string SnapshotImageName2 {
            get { return _snapshotImageName2; }
            set { _snapshotImageName2 = value; }
        }

        /// <summary>
        /// The sub-raster file (512x512 cells). This is a helper file
        /// for creating the .bil file
        /// </summary>
        public string SnapshotRasterName {
            get { return _snapshotRasterName; }
            set { _snapshotRasterName = value; }
        }

        /// <summary>
        /// The binary file used by the TerrainViewer.exe to read the 3d-data
        /// </summary>
        public string BilFileName {
            get { return _bilFileName; }
            set { _bilFileName = value; }
        }

        /// <summary>
        /// The xml file with configuration of the TerrainViewer.exe
        /// </summary>
        public string XmlFileName {
            get { return _xmlFileName; }
            set { _xmlFileName = value; }
        }

        /// <summary>
        /// The main map of the DotSpatial application
        /// </summary>
        public Map MainMap {
            get { return _mainMap; }
            set { _mainMap = value; }
        }

        /// <summary>
        /// The full path to the TerrainViewer.exe file
        /// </summary>
        public string TerrainViewerExeFile {
            get { return _terrainViewerExeFile; }
            set { _terrainViewerExeFile = value; }
        }

        /// <summary>
        /// This is the main function. This runs the 3d-view.
        /// mouse x is the x-coordinate in screen units where user clicked.
        /// mouse y is the y-coordinate in screen units where user clicked.
        /// </summary>
        public void Run3dView(int mouseX, int mouseY) {
            
            //find the raster layers in the map
            IMapRasterLayer[] rasters = MainMap.GetRasterLayers();
            if (rasters.Length == 0)
            {
                MessageBox.Show("No raster layers were found in the map. Please add a raster layer first.");
                return;
            }
            //use the first raster layer for the terrain
            _originalGrid = rasters[0].DataSet as Raster;

            //convert the mouse coordinates to map projected coordinates
            Coordinate co = MainMap.PixelToProj(new System.Drawing.Point(mouseX, mouseY));
            double xProj = co.X;
            double yProj = co.Y;

            //create the 512x512 cell grid for the 3d-viewer
            CreateSnapshot(_originalGrid, SNAPSHOT_GRID_SIZE, xProj, yProj);

            //check if all input files for TerrainViewer.exe exist
            // does the xml file exist?
            string errorText = "There was an error launching the 3D viewer. ";
            if (!File.Exists(_xmlFileName)) {
                MessageBox.Show(errorText + _xmlFileName + " was not found.");
                return;
            }
            // does the large bmp file exist?
            if (!File.Exists(_snapshotImageName1)) {
                MessageBox.Show(errorText + _snapshotImageName1 + " was not found.");
                return;
            }
            // does the small bmp file exist?
            if (!File.Exists(_snapshotImageName2)) {
                MessageBox.Show(errorText + _snapshotImageName2 + " was not found.");
                return;
            }
            // does the TerrainViewer.exe file exist?
            if (!File.Exists(_terrainViewerExeFile)) {
                MessageBox.Show(errorText + " The program TerrainViewer.exe was not found: " + _terrainViewerExeFile);
                return;
            }

            //Run the file terrainViewer.exe with the proper command-line arguments
            try
            {
                string args = _xmlFileName;
                System.Diagnostics.Process.Start(_terrainViewerExeFile, args);
            }
            catch (Exception ex)
            {
                MessageBox.Show("There was an error launching the 3D viewer. Please check that DirectX is installed. " + ex.Message);
            }
        }


        /// <summary>
        /// Gets a grid square sub-region and saves it to a .bil file which is used by the 3D viewer
        /// </summary>
        /// <param name="gr">the grid</param>
        /// <param name="xCenter">center x point of the region shown by 3D viewer</param>
        /// <param name="yCenter">center y point of the region shown by 3D viewer</param>
        /// <param name="numRows">number of rows in the grid</param>
        public void CreateSnapshot(Raster gr, int numRows, double xCenter, double yCenter) {

            //Step 1: create snapshot boundary line and add it to the map
            CreateSnapshotBoundary(xCenter, yCenter, numRows, gr.CellWidth);
            
            bool gridWindowCreated = false;

            //Step 2: get the center coordinates in cells
            int centerRow = 0;
            int centerCol = 0;
            int nr = gr.NumRows;
            int nc = gr.NumColumns;

            RcIndex idx = gr.ProjToCell(xCenter, yCenter);
            centerRow = idx.Row;
            centerCol = idx.Column;

            //Check if we are inside grid
            if (centerRow < 0 || centerCol < 0 || centerRow > nr || centerCol > nc)
            {
                MessageBox.Show("You clicked outside of the grid boundary. please click inside the grid to display the 3D View.");
                return;
            }

            int halfNumRows = numRows / 2;

            int startRow = centerRow - halfNumRows;
            int startCol = centerCol - halfNumRows;
            int endRow = startRow + numRows - 1;
            int endCol = startCol + numRows - 1;

            float[] gridWindow = new float[numRows * numRows];
            //IRaster gridWindow;

            // Step 3: create the snapshot raster (for the overlay image)
            IRaster snapshotRaster = Raster.Create(_snapshotRasterName, "bgd", numRows, numRows, 1, typeof(float), new string[] { });

            try
            {
                int indx = 0;
                int rowIndex = 0;
                int colIndex = 0;

                for (int r = startRow; r <= endRow; r++)
                {
                    colIndex = 0;
                    for (int c = startCol; c <= endCol; c++)
                    {

                        gridWindow[indx] = (float)gr.Value[r, c];

                        snapshotRaster.Value[rowIndex, colIndex] = gridWindow[indx];
                        indx++;
                        colIndex++;
                    }
                    rowIndex++;
                }

                snapshotRaster.Save();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Unable to display 3D view " + ex.Message);
                return;
            }

            //Step 4: save grid to a .bil file (multiply all values by 100 to get short integer values)
            double cellSize = gr.CellWidth;

            //multiplication factor depends on the height span
            double minimum = Convert.ToDouble(gr.Minimum);
            double maximum = Convert.ToDouble(gr.Maximum);
            double span = maximum - minimum;
            int multFactor = 1;
            if (span < 100)
            {
                multFactor = 100;
            }

            //check for Latitude/Longitude projection
            //commented out - this was causing exaggeration of the raster.
            //string proj = gr.Projection.ToProj4String().ToLower();//gr.Header.Projection.ToLower();
            //if (proj != "")
            //{
            //    if (proj.IndexOf("latlong") >= 0 || proj.IndexOf("longlat") >= 0)
            //    {
            //        //100000 is an approximate latitude degree to meter conversion factor
            //        cellSize = cellSize / 100000;
            //    }
            //}


            //create a 1d-array with same number of elements as the sub-raster
            short[] shortArray = new short[gridWindow.Length];

            int index = 0;
            for (int i = 0; i < shortArray.Length; i++)
            {
                float val = gridWindow[i];

                if (val > minimum)
                {
                    shortArray[i] = (short)((val - minimum) * multFactor);
                }
                else
                {
                    shortArray[i] = (short)(0 * multFactor);
                }
                index++;
            }

            //write the array to the .bil file
            int outLenBytes = 2 * shortArray.Length;
            byte[] outBytes = new byte[outLenBytes];
            Buffer.BlockCopy(shortArray, 0, outBytes, 0, outLenBytes);
            System.IO.BinaryWriter binWriter = new System.IO.BinaryWriter(File.Open(_bilFileName, FileMode.Create));

            try
            {
                binWriter.Write(outBytes);
                gridWindowCreated = true;
            }
            catch (Exception ex)
            {
                MessageBox.Show("There was an error loading 3D viewer data. " + ex.Message);
            }
            finally
            {
                binWriter.Flush();
                binWriter.Close();
            }

            if (gridWindowCreated == false) return;

            //Step 5: Create the snapshot image file for the 3d view overlay
            MapRasterLayer lay = new MapRasterLayer(snapshotRaster);
            RasterSymbolizer sym = (RasterSymbolizer)lay.Symbolizer;
            Bitmap trialbmp = sym.CreateBitmap();
            trialbmp.Save(_snapshotImageName1);

            //Step 6: resize the snapshot image for the overview map
            Image smallbmp = ResizeImage(105, 105, trialbmp);
            smallbmp.Save(_snapshotImageName2);


            //Step 7: Create a maps.xml xml file
            double elevFactor = 1 / multFactor / cellSize;

            if (elevFactor <= 0)
            {
                elevFactor = 2;
            }

            if (elevFactor > 20)
            {
                elevFactor = 2;
            }
            CreateMapXmlFile(elevFactor);
        }

        /// <summary>
        /// Adds a special '3d view area' layer to the map. This layer show the area, which is displayed
        /// by the 3d-view window.
        /// xCenter, yCenter are the coordinates of the center of the 3d view area in map coordinates
        /// grid is the raster that is used for the 3d view. gridCellSize is the cell size
        /// </summary>
        public void CreateSnapshotBoundary(double xCenter, double yCenter, int numRows, double gridCellSize) {

            IMap map = MainMap;
            
            try
            {
                string layerName3d = "3d-view-boundary"; 
                //remove the boundary layer if it exists
                foreach(IMapLineLayer lineLayer in map.GetLineLayers())
                {
                    if (lineLayer.LegendText == layerName3d)
                    {
                        map.Layers.Remove(lineLayer);
                    }
                }
                
                //calculate xMin, yMin, xMax, yMax coordinates
                int halfNumRows = numRows / 2;
                double distanceFromCenter = halfNumRows * gridCellSize;
                double xMin = xCenter - distanceFromCenter;
                double xMax = xCenter + distanceFromCenter;
                double yMin = yCenter - distanceFromCenter;
                double yMax = yCenter + distanceFromCenter;
                
                //create the boundary line shape
                int numPoints = 5;
                Coordinate[] coords = new Coordinate[numPoints];
                coords[0] = new Coordinate(xMin, yMin);
                coords[1] = new Coordinate(xMin, yMax);
                coords[2] = new Coordinate(xMax, yMax);
                coords[3] = new Coordinate(xMax, yMin);
                coords[4] = new Coordinate(xMin, yMin);

                FeatureSet boundaryFeatureSet = new FeatureSet(FeatureType.Line);
                boundaryFeatureSet.Projection = map.Projection;
                boundaryFeatureSet.AddFeature(new LineString(coords));

                //add boundary layer to map and set its legend text
                ILayer ly = map.Layers.Add(boundaryFeatureSet);
                ly.LegendText = layerName3d;
                
            }
            catch (Exception ex)
            {
                MessageBox.Show("Unable to load 3D viewer. There was an error creating the snapshot of the map." + ex.Message);
                return;
            }
        }

        /// <summary>
        /// Removes the 3d view area boundary layer from the map
        /// </summary>
        public void RemoveSnapshotBoundary() {
            foreach (IMapLineLayer lineLayer in MainMap.GetLineLayers())
            {
                if (lineLayer.LegendText == _boundaryLayerName)
                {
                    MainMap.Layers.Remove(lineLayer);
                }
            }
        }

        //resizes the image for the 'overview' window
        public Image ResizeImage(int newWidth, int newHeight, Image imgPhoto) {

            int sourceWidth = imgPhoto.Width;
            int sourceHeight = imgPhoto.Height;

            //Consider vertical pics
            if (sourceWidth < sourceHeight)
            {
                int buff = newWidth;

                newWidth = newHeight;
                newHeight = buff;
            }

            int sourceX = 0, sourceY = 0, destX = 0, destY = 0;
            float nPercent = 0, nPercentW = 0, nPercentH = 0;

            nPercentW = ((float)newWidth / (float)sourceWidth);
            nPercentH = ((float)newHeight / (float)sourceHeight);
            if (nPercentH < nPercentW)
            {
                nPercent = nPercentH;
                destX = System.Convert.ToInt16((newWidth -
                          (sourceWidth * nPercent)) / 2);
            }
            else
            {
                nPercent = nPercentW;
                destY = System.Convert.ToInt16((newHeight -
                          (sourceHeight * nPercent)) / 2);
            }

            int destWidth = (int)(sourceWidth * nPercent);
            int destHeight = (int)(sourceHeight * nPercent);


            Bitmap bmPhoto = new Bitmap(newWidth, newHeight,
                          PixelFormat.Format24bppRgb);

            bmPhoto.SetResolution(imgPhoto.HorizontalResolution,
                         imgPhoto.VerticalResolution);

            Graphics grPhoto = Graphics.FromImage(bmPhoto);
            grPhoto.Clear(Color.Black);
            grPhoto.InterpolationMode =
                InterpolationMode.HighQualityBicubic;

            grPhoto.DrawImage(imgPhoto,
                new Rectangle(destX, destY, destWidth, destHeight),
                new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight),
                GraphicsUnit.Pixel);

            grPhoto.Dispose();
            return bmPhoto;
        }

        /// <summary>
        /// Creates a special 'maps.xml' file used by the 3D terrain viewer program
        /// </summary>
        private void CreateMapXmlFile(double altScale) {
            XmlTextWriter xmlWriter = new XmlTextWriter(_xmlFileName, System.Text.Encoding.UTF8);
            xmlWriter.Formatting = Formatting.Indented;
            xmlWriter.WriteProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
            xmlWriter.WriteStartElement("maps");
            xmlWriter.Close();

            XmlDocument doc = new XmlDocument();
            doc.Load(_xmlFileName);

            XmlElement root = doc.DocumentElement;

            XmlElement mapElement = doc.CreateElement("map");
            mapElement.SetAttribute("name", "");

            XmlElement terrainElement = doc.CreateElement("terrain");
            terrainElement.SetAttribute("demfile", "snapshot.bil");
            terrainElement.SetAttribute("texturefile", "snapshot.bmp");
            terrainElement.SetAttribute("altscale", altScale.ToString());

            root.AppendChild(mapElement);
            mapElement.AppendChild(terrainElement);

            doc.Save(_xmlFileName);

            StringBuilder str = new StringBuilder();
            str.AppendLine("?xml version='1.0' ?>");
        }
    }
}